﻿using System;
using System.Reflection;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Windows.Input;
#if WINDOWS_PHONE
using System.ComponentModel;
using System.Windows;
#else
using Windows.UI.Xaml;
#endif


namespace MetroEventToCommand
{
    public class EventToCommand : FrameworkElement
    {
        // if HandleProperty = true but no Handled property, this will be true so we don't try again
        private bool _forcedonohandle;

        private PropertyInfo _handledProperty;

        #region RelayCommand

        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register("Command", typeof(ICommand), typeof(EventToCommand),
                                        new PropertyMetadata(DependencyProperty.UnsetValue));

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        #endregion

        #region RelayCommandParameter

        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register("CommandParameter", typeof(object), typeof(EventToCommand),
                                        new PropertyMetadata(DependencyProperty.UnsetValue));

        public object CommandParameter
        {
            get { return GetValue(CommandParameterProperty); }
            set { SetValue(CommandParameterProperty, value); }
        }

        #endregion

        #region RelayCommandEvent

        public static readonly DependencyProperty EventProperty =
            DependencyProperty.Register("Event", typeof(string), typeof(EventToCommand),
                                        new PropertyMetadata(DependencyProperty.UnsetValue));

        private WeakReference<FrameworkElement> _element;

        public string Event
        {
            get { return (string)GetValue(EventProperty); }
            set { SetValue(EventProperty, value); }
        }

        #endregion

        #region RelayCommandHandleEvent

        public static readonly DependencyProperty HandleEventProperty =
            DependencyProperty.Register("HandleEvent", typeof(bool), typeof(EventToCommand),
                                        new PropertyMetadata(false));
        public bool HandleEvent
        {
            get { return (bool)GetValue(HandleEventProperty); }
            set { SetValue(HandleEventProperty, value); }
        }

        #endregion

        public FrameworkElement Element
        {
            get
            {
                FrameworkElement item;
                _element.TryGetTarget(out item);
                return item;
            }
            set
            {
                _element = new WeakReference<FrameworkElement>(value);

#if WINDOWS_PHONE
                if (DesignerProperties.IsInDesignTool)
                    return;
#else
                if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
                    return;
#endif

                if ((value == null) || (string.IsNullOrWhiteSpace(Event)))
                    return;

                var eventinfo = Element.GetType().GetRuntimeEvent(Event);

                // if event not found, throw exception.
                if (eventinfo == null)
                    throw new ArgumentException(string.Format("Event {0} not found on element {1}.", Event, Element.GetType().Name));

                // Get method to call when event raised
                var executemethodinfo = GetType().GetTypeInfo().GetDeclaredMethod("ExecuteCommand").CreateDelegate(eventinfo.EventHandlerType, this);

                // In Windows Phone, the add_method of event is returning null, not an EventRegistrationToken. So ave to do in a less clean way.
                // I could do the same for Win8, but I prefer to separate so I can keep the clean version for Win8
#if WINDOWS_PHONE
                // Register event (Quick & dirty way)
                WindowsRuntimeMarshal.AddEventHandler(
                    del =>
                    {
                        eventinfo.AddEventHandler(Element, executemethodinfo);

                        return new EventRegistrationToken();
                    },
                    token => eventinfo.RemoveEventHandler(Element, executemethodinfo), executemethodinfo);
#else
                // Register event
                WindowsRuntimeMarshal.AddEventHandler(
                    del => (EventRegistrationToken)eventinfo.AddMethod.Invoke(Element, new object[] { del }),
                    token => eventinfo.RemoveMethod.Invoke(Element, new object[] { token }), executemethodinfo);


#endif
            }
        }

        public void ExecuteCommand(object sender, object eventargs)
        {
            var parsingok = false;

            // TypeInfo of the parameter
            TypeInfo parametertypeinfo;

            // If parameter is a EventParameter, this will store the type (used for dynamic instanciation)
            Type eventgenerictype = null;

            // The typed parameter
            object typedparameter = null;

            // If datacontext is null, use the parent element one.
            if (DataContext == null)
                DataContext = Element.DataContext;

            //Command is null, so don't do anything
            if (Command == null)
                return;

            // Get the parameter
            var parameter = CommandParameter;

            if (parameter != null)
            {
                parametertypeinfo = Command.GetType().GetTypeInfo();
                // Check if parameter is Generic type
                if (parametertypeinfo.IsGenericType)
                {
                    // Get the generic argument type
                    var generictype = parametertypeinfo.GenericTypeArguments[0];

                    // It is an EventParameter. So save the type and set the generictype to the EventParameter generic type
                    if (generictype.FullName.StartsWith("MetroEventToCommand.EventParameter`1"))
                    {
                        eventgenerictype = generictype;
                        generictype = generictype.GenericTypeArguments[0];
                    }

                    // If the generic type is not string, and the parameter is string, let's cast
                    // If RelayCommandParameter is a direct input and not a binding, its value is of type string
                    // direct input is most probably a type (like int,etc...) so I'll try to cast to type
                    if ((generictype != typeof(string) && (CommandParameter is string)))
                    {
                        switch (generictype.Name)
                        {
                            case "Boolean":
                                bool boolresult;
                                parsingok = bool.TryParse(CommandParameter.ToString(), out boolresult);
                                if (parsingok)
                                {
                                    typedparameter = boolresult;
                                }
                                break;
                            case "Byte":
                                byte byteresult;
                                parsingok = byte.TryParse(CommandParameter.ToString(), out byteresult);
                                if (parsingok)
                                {
                                    typedparameter = byteresult;
                                }
                                break;
                            case "SByte":
                                sbyte sbyteresult;
                                parsingok = sbyte.TryParse(CommandParameter.ToString(), out sbyteresult);
                                if (parsingok)
                                {
                                    typedparameter = sbyteresult;
                                }
                                break;
                            case "Char":
                                char charresult;
                                parsingok = char.TryParse(CommandParameter.ToString(), out charresult);
                                if (parsingok)
                                {
                                    typedparameter = charresult;
                                }
                                break;
                            case "Decimal":
                                decimal decimalresult;
                                parsingok = decimal.TryParse(CommandParameter.ToString(), out decimalresult);
                                if (parsingok)
                                {
                                    typedparameter = decimalresult;
                                }
                                break;
                            case "Double":
                                double doubleresult;
                                parsingok = double.TryParse(CommandParameter.ToString(), out doubleresult);
                                if (parsingok)
                                {
                                    typedparameter = doubleresult;
                                }
                                break;
                            case "Single":
                                Single Singleresult;
                                parsingok = Single.TryParse(CommandParameter.ToString(), out Singleresult);
                                if (parsingok)
                                {
                                    typedparameter = Singleresult;
                                }
                                break;
                            case "Int32":
                                int intresult;
                                parsingok = int.TryParse(CommandParameter.ToString(), out intresult);
                                if (parsingok)
                                {
                                    typedparameter = intresult;
                                }
                                break;
                            case "UInt32":
                                UInt32 UInt32result;
                                parsingok = UInt32.TryParse(CommandParameter.ToString(), out UInt32result);
                                if (parsingok)
                                {
                                    typedparameter = UInt32result;
                                }
                                break;
                            case "Int64":
                                Int64 Int64result;
                                parsingok = Int64.TryParse(CommandParameter.ToString(), out Int64result);
                                if (parsingok)
                                {
                                    typedparameter = Int64result;
                                }
                                break;
                            case "UInt64":
                                UInt64 UInt64result;
                                parsingok = UInt64.TryParse(CommandParameter.ToString(), out UInt64result);
                                if (parsingok)
                                {
                                    typedparameter = UInt64result;
                                }
                                break;
                            case "Int16":
                                Int16 Int16result;
                                parsingok = Int16.TryParse(CommandParameter.ToString(), out Int16result);
                                if (parsingok)
                                {
                                    typedparameter = Int16result;
                                }
                                break;
                            case "UInt16":
                                UInt16 UInt16result;
                                parsingok = UInt16.TryParse(CommandParameter.ToString(), out UInt16result);
                                if (parsingok)
                                {
                                    typedparameter = UInt16result;
                                }
                                break;
                        }

                        if (!parsingok)
                        {
                            throw new ArgumentException(string.Format("Wrong parameter type. {0} expected, {1} received", generictype.Name, CommandParameter.GetType().Name));
                        }
                    }
                    else
                        typedparameter = CommandParameter;

                    // In case it is an EventParameter
                    if (eventgenerictype != null)
                    {
                        // Instanciate an EventParameter with data
                        var eventparameter = Activator.CreateInstance(eventgenerictype, new[]
                                                                  {
                                                                      sender, eventargs, typedparameter
                                                                  });
                        Execute(eventparameter, eventargs);
                        return;
                    }

                }
            }

            // Execute command
            Execute(typedparameter,eventargs);

        }

        private void Execute(object param,object eventargs)
        {
           // Check if need to handle event
            if (HandleEvent && !_forcedonohandle)
            {
                // If _handledProperty, get the PropertyInfo
                if (_handledProperty == null)
                    _handledProperty = eventargs.GetType().GetRuntimeProperty("Handled");

                //_handledProperty is null, then force not handling
                if (_handledProperty == null)
                {
                    _forcedonohandle = true;
                }
                else
                {
                    _handledProperty.SetValue(eventargs, true);
                }
            }
            // Execute command
            Command.Execute(param);
        }
    }
}
